﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MpqLib.Mpq;
using System.IO;
using System.Drawing;
namespace Kralizek.StarCraft2.ReplayParser
{
    public static class ReplayParser
    {
        public static GameInfo Load(string path)
        {
            ASCIIEncoding enc = new ASCIIEncoding();
            GameInfo res = null;
            byte[] bReplayInfo;
            byte[] bMapImage;

            if (!File.Exists(path))
                throw new ArgumentException("The specified file does not exists", "path");

            using (var archive = new CArchive(path))
            {
                bReplayInfo = ExtractReplayInfo(archive);
                bMapImage = ExtractMapImage(archive);
            }

            List<Player> players = new List<Player>();
            GameSpeed gameSpeed;
            MapInfo Map = null;
            Image image = null;

            using (MemoryStream ms = new MemoryStream(bMapImage))
            {
                image = Image.FromStream(ms);
            }

            using (MemoryStream ms = new MemoryStream(bReplayInfo))
            {
                if (ms.ReadByte() != 0x10)
                    throw new InvalidReplayFileException();

                using (BinaryReader reader = new BinaryReader(ms))
                {
                    // Extract the human players' names
                    List<string> humanPlayers = new List<string>();
                    do
                    {
                        int length = ms.ReadByte();

                        string name = new string(reader.ReadChars(length));
                        reader.ReadBytes(5);

                        humanPlayers.Add(name);
                    } while (reader.PeekChar() != 0x0);

                    // Let's skip the first void zone
                    while (reader.ReadByte() == 0x0) ;
                    ms.Position--;

                    var unknown1 = reader.ReadBytes(12);

                    gameSpeed = (GameSpeed)reader.ReadByte();

                    // Code used for getting a well-known position
                    long pos = SeekSequence(bReplayInfo, ms.Position, new byte[] { 0x73, 0x32, 0x6d, 0x61 }, 6);
                    ms.Seek(pos, SeekOrigin.Begin);
                    reader.ReadBytes(10);
                    pos = SeekSequence(bReplayInfo, ms.Position, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0 });
                    ms.Seek(pos, SeekOrigin.Begin);

                    while (reader.PeekChar() < 30)
                        reader.ReadByte();
                    
                    string mapName = "";
                    while (reader.PeekChar() != 0x0)
                        mapName += reader.ReadChar();

                    Map = new MapInfo { Name = mapName, Thumbnail = image };

                    // Skip the header
                    reader.ReadBytes(5);

                    do
                    {
                        // Get the length of the name
                        int nameLength = reader.ReadByte();
                        string name = new string(reader.ReadChars(nameLength));

                        // Get the length of the name of the race
                        int raceLength = reader.ReadByte();
                        string raceName = new string(reader.ReadChars(raceLength));

                        // Get the length of the color
                        int colorLength = reader.ReadByte();
                        var color = enc.GetString(reader.ReadBytes(colorLength)).Split(',').Select(s => int.Parse(s)).ToArray();

                        Player player = new Player
                        {
                            Name = name,
                            Race = GetRaceFromName(raceName),
                            Color = Color.FromArgb(color[0],color[1],color[2],color[3]),
                            IsHuman = humanPlayers.Contains(name)
                        };

                        players.Add(player);

                    } while (reader.PeekChar() != 0x0 && ms.Position < ms.Length);

                    
                }
            }

            res = new GameInfo
                {
                    Players = players,
                    GameSpeed = gameSpeed,
                    Map = Map
                };
            return res;
        }

        private static long SeekSequence(byte[] source, long startIndex, byte[] sequence) { return SeekSequence(source, startIndex, sequence, 1); }

        private static long SeekSequence(byte[] source, long startIndex, byte[] sequence, int nth)
        {
            long pos = -1;
            long start = startIndex;

            for (int z = 0; z < nth; z++)
            {
                for (long i = start; i < source.LongLength - sequence.Length; i++)
                {
                    for (int y = 0; y < sequence.Length; y++)
                    {
                        if (source[i + y] != sequence[y])
                            break;

                        if (y == sequence.Length - 1)
                        {
                            pos = i;

                            if (pos > -1 && z == nth - 1)
                                return i;
                        }
                    }
                }

                start = pos;
            }

            return pos;
        }

        private static Race GetRaceFromName(string raceName)
        {
            Race res;

            switch (raceName.ToLower())
            {
                case "terran":
                    res = Race.Terran;
                    break;
                case "zerg":
                    res = Race.Zerg;
                    break;
                case "protoss":
                    res = Race.Protoss;
                    break;
                default:
                    throw new ArgumentException("Invalid argument", "raceName");
            }

            return res;
        }

        private static byte[] ExtractReplayInfo(CArchive archive)
        {
            var files = archive.FindFiles("replay.info");
            byte[] buffer;

            if (files.Any())
            {
                var file = files.Single();
                buffer = new byte[file.Size];
                archive.ExportFile("replay.info", buffer);
            }
            else
            {
                throw new FileNotFoundException("The archive does not contain the requested file", "replay.info");
            }

            return buffer;
        }


        private static byte[] ExtractMapImage(CArchive archive)
        {
            var files = archive.FindFiles("save.jpg");
            byte[] buffer;

            if (files.Any())
            {
                var file = files.Single();
                buffer = new byte[file.Size];
                archive.ExportFile("save.jpg", buffer);
            }
            else
            {
                throw new FileNotFoundException("The archive does not contain the requested file", "replay.info");
            }

            return buffer;
        }
    }

    public class InvalidReplayFileException : ApplicationException { }
}
